package create

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path"
	"regexp"
	"strings"
	"text/template"

	"gitee.com/shuokeyun/kratos/cmd/sk-kratos/internal/base"
	"github.com/AlecAivazis/survey/v2"
	"github.com/spf13/cobra"
	"gopkg.in/yaml.v2"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var CmdCreate = &cobra.Command{
	Use:   "create",
	Short: "Create structure mappings for database tables",
	Long:  "Create structure mappings for database tables. Example: sk-kratos table create * -c=configs/config.yaml -t=internal/data/po",
	Run:   run,
}

var configFile string
var dbSource string
var outDir string
var deleteTablePrefix string
var tablePrefix string
var tableSuffix string

func init() {
	CmdCreate.Flags().StringVarP(&configFile, "config", "c", "", "config.yaml path")
	CmdCreate.Flags().StringVarP(&dbSource, "source", "s", "", "mysql source")
	CmdCreate.Flags().StringVarP(&outDir, "target-dir", "t", "", "out dir")
	CmdCreate.Flags().StringVarP(&deleteTablePrefix, "delete-table-prefix", "d", "", "delete table name prefix")
	CmdCreate.Flags().StringVarP(&tablePrefix, "prefix", "p", "", "struct name prefix")
	CmdCreate.Flags().StringVarP(&tableSuffix, "suffix", "u", "", "struct name suffix")
}

func run(cmd *cobra.Command, args []string) {
	if len(args) == 0 {
		log.Fatalf("Please specify the table")
	}
	tableName := args[0]
	if outDir == "" {
		outDir = path.Join(findProRoot(), "internal/data/po")
	}
	source := parseMysqlSource()
	reg := regexp.MustCompile(`^.+:.+@tcp\(.+\)/(.+)[:^\?:]`)
	match := reg.FindStringSubmatch(source)
	if len(match) < 2 {
		log.Fatalf("Failed to resolve the database name：%s\n", source)
	}
	dbName := match[1]
	DB, err := gorm.Open(mysql.Open(source))
	if err != nil {
		log.Fatalf("Database connection failure：%s\n", err.Error())
	}
	db, err := DB.DB()
	if err != nil {
		log.Fatalf("Database connection failure：%s\n", err.Error())
	}
	defer db.Close()
	lastBarIndex := strings.LastIndex(outDir, "/")
	packageName := outDir
	if lastBarIndex > -1 {
		packageName = outDir[lastBarIndex+1:]
	}
	data := readTableStruct(DB, dbName, tableName, packageName)
	if _, err := os.Stat(outDir); os.IsNotExist(err) {
		err = os.MkdirAll(outDir, 0744)
		if err != nil {
			log.Fatalf("Directory %s create failure: %s", outDir, err.Error())
		}
	}
	wd, _ := os.Getwd()
	for _, v := range data {
		t, err := template.New("table").Parse(tmp)
		if err != nil {
			log.Fatalf("Template parsing failure：%s\n", err.Error())
		}
		buf := new(bytes.Buffer)
		err = t.Execute(buf, v)
		if err != nil {
			log.Fatalf("Template parsing failure：%s\n", err.Error())
		}
		s := strings.Replace(buf.String(), "${Backquote}", "`", -1)
		fname := v.DoName
		if tableSuffix != "" {
			sIndex := strings.LastIndex(v.DoName, tableSuffix)
			fname = v.DoName[:sIndex]
			fname = base.Camel2Case(fname) + "_" + strings.ToLower(tableSuffix)
		} else {
			fname = base.Camel2Case(fname)
		}
		doFile := path.Join(outDir, fname+".go")
		if _, err = os.Stat(doFile); !os.IsNotExist(err) {
			override := false
			survey.AskOne(&survey.Confirm{
				Message: fmt.Sprintf("File %s already exists. Do you want to override the file", doFile),
				Default: false,
			}, &override)
			if !override {
				continue
			}
		}
		err = ioutil.WriteFile(doFile, []byte(s), 0644)
		if err != nil {
			log.Printf("File %s generation failure: %s", doFile, err.Error())
		}
		base.Gofmt(doFile)
		log.Printf("%s success\n", strings.TrimLeft(strings.Replace(doFile, wd, "", 1), "/"))
	}
}

func readTableStruct(DB *gorm.DB, db string, tables string, packageName string) map[string]*parseStr {
	var tableCommentList []tableComment
	tableList := strings.Split(tables, ",")
	var wh []string
	for _, v := range tableList {
		if strings.Index(v, "%") != -1 {
			wh = append(wh, fmt.Sprintf("table_name like '%s'", v))
		} else {
			wh = append(wh, fmt.Sprintf("table_name = '%s'", v))
		}
	}
	err := DB.Table("information_schema.tables").Select("table_name as table_name,table_comment as table_comment").Where("table_schema", db).Where(strings.Join(wh, " or ")).Scan(&tableCommentList).Error
	if err != nil {
		log.Fatalf("Description Failed to query the table structure：%s\n", err.Error())
	}
	var li []schema
	err = DB.Table("information_schema.columns").Select("table_schema as table_schema, table_name as table_name, column_name as column_name, data_type as data_type,column_comment as column_comment,column_key as column_key").Where("table_schema", db).Where(strings.Join(wh, " or ")).Scan(&li).Error
	if err != nil {
		log.Fatalf("Description Failed to query the table structure：%s\n", err.Error())
	}
	if len(li) == 0 {
		log.Fatalln("No tables were found")
	}
	reply := make(map[string]*parseStr)
	for _, v := range tableCommentList {
		dl := strings.Split(deleteTablePrefix, ",")
		doName := strings.Title(tablePrefix)
		for _, pre := range dl {
			if strings.HasPrefix(v.TableName, pre) {
				doName += case2Camel(strings.Replace(v.TableName, pre, "", -1))
				break
			}
		}
		doName += tableSuffix
		reply[v.TableName] = &parseStr{
			PackageName:   packageName,
			FullTableName: v.TableName,
			TableRemark:   v.TableComment,
			DoName:        doName,
		}
	}
	for _, v := range li {
		reply[v.TableName].ColumnList = append(reply[v.TableName].ColumnList, conversion(v))
	}
	return reply
}

type parseStr struct {
	PackageName   string
	FullTableName string
	TableRemark   string
	DoName        string
	ColumnList    []*outSchema
}

type schema struct {
	TableSchema   string
	TableName     string
	ColumnName    string
	DataType      string
	ColumnComment string
	ColumnKey     string
}
type tableComment struct {
	TableName    string
	TableComment string
}

func parseMysqlSource() string {
	if dbSource != "" {
		return dbSource
	}
	if configFile == "" {
		configFile = path.Join(findProRoot(), "configs", "config.yaml")
		_, err := os.Stat(configFile)
		if os.IsNotExist(err) {
			log.Fatalf("The configuration file %s does not exist\n", configFile)
		}
	}
	by, err := ioutil.ReadFile(configFile)
	if err != nil {
		log.Fatalf("The configuration file %s does not exist. using the -c parameter to specify the config address or -s parameter to specify the database connection address\n", configFile)
	}
	r := struct {
		Data struct {
			Database struct {
				Source string
			}
		}
	}{}
	err = yaml.Unmarshal(by, &r)
	if err != nil {
		log.Fatalf("Configuration file %s parsing failed, please check the format\n", configFile)
	}
	return r.Data.Database.Source
}

func findProRoot() string {
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	i := 0
	for {
		_, err = os.Stat(path.Join(wd, "go.mod"))
		if os.IsNotExist(err) {
			wd = path.Dir(wd)
			i++
			if wd == "/" || i == 5 {
				log.Fatalln("Configuration file not found")
			}
		} else {
			break
		}
	}
	return wd
}

type outSchema struct {
	Column    string
	OrgColumn string
	Type      string
	Comment   string
	IsPri     bool
}

func conversion(v schema) *outSchema {
	return &outSchema{
		Column:    case2Camel(v.ColumnName),
		OrgColumn: v.ColumnName,
		Type:      convType(v.DataType),
		Comment:   strings.TrimSpace(v.ColumnComment),
		IsPri:     v.ColumnKey == "PRI",
	}
}

func convType(ty string) string {
	switch ty {
	case "int", "bigint":
		return "int"
	case "tinyint":
		return "int8"
	case "smallint":
		return "int16"
	case "decimal":
		return "float64"
	case "datetime":
		return "time"
	default:
		return "string"
	}
}

func case2Camel(name string) string {
	name = strings.Replace(name, "_", " ", -1)
	name = strings.Title(name)
	return strings.Replace(name, " ", "", -1)
}

var tmp = `package {{.PackageName}}

// {{.DoName}} {{.FullTableName}} {{.TableRemark}}
type {{.DoName}} struct { {{range .ColumnList}}
	{{.Column}} {{.Type}} ${Backquote}gorm:"{{- if .IsPri }}primaryKey;{{- end }}column:{{.OrgColumn}}"${Backquote} {{- if .Comment }}// {{.Comment}}{{- end }}{{end}}
}
`
